package com.dcivision.lucene;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.sql.Connection;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.Hits;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.Sort;
import org.apache.lucene.search.TermQuery;

import com.dcivision.dms.bean.DmsContent;
import com.dcivision.dms.bean.DmsDocument;
import com.dcivision.framework.ApplicationException;
import com.dcivision.framework.ErrorConstant;
import com.dcivision.framework.SessionContainer;
import com.dcivision.framework.Utility;
import com.dcivision.lucene.highlighter.DmsHighlighter;

/**
 * IndexManager class could be called by multiply user thread concurrently.
 * class
 * @author Administrator
 */
public class IndexManager {

  private static Log log = LogFactory.getLog(IndexManager.class);    
  
  private SessionContainer sessionContainer = null;
  
  private Connection conn = null;
  
  private IndexOperation indexOperation = null;
  
  private IndexSearcher indexSearcher = null;
  
  private IndexReader indexReadOnlyReader = null;
  
  private int batchSize = 500;  
  
  public IndexManager(SessionContainer sessionContainer, Connection conn) {
    this.sessionContainer = sessionContainer;
    this.conn = conn;
  }
  
  private IndexReader getReadOnlyIndexReader() throws ApplicationException {
    try {
      if (this.indexReadOnlyReader == null) {
        this.indexReadOnlyReader = IndexReader.open(IndexFactory.getDirectory());
      }
    }catch (Exception ex) {
      throw new ApplicationException(ErrorConstant.INDEX_CANNOT_READ_INDEX, ex, ex.toString());
    }
    return this.indexReadOnlyReader;
  }
  
  /**
   * Creates a searcher searching the index from IndexFactory.
   * @return
   * @throws ApplicationException
   */
  private IndexSearcher getIndexSearcher() throws ApplicationException {
    try {
      if (this.indexSearcher == null) {
        this.indexSearcher = new IndexSearcher(IndexFactory.getDirectory());
      }
    }catch (Exception ex) {
      throw new ApplicationException(ErrorConstant.INDEX_CANNOT_READ_INDEX, ex, ex.toString());
    }
    return this.indexSearcher;
  }
  
  /**
   * Create a operation to handle business logic.
   * @return
   */
  private IndexOperation getIndexOperation() {
    if (this.indexOperation == null) {
      this.indexOperation = new IndexOperation(sessionContainer, conn);
    }
    return this.indexOperation;
  }
  
  /**
   * Get default analyzer form facotry class.
   * @return
   */
  public Analyzer getAnalyzer() {
    return IndexFactory.getAnalyzer();
  }
  
  /**
   * Get lucene document count.
   * @return
   * @throws ApplicationException
   */
  public int getDocCount() throws ApplicationException {
    try {
      return IndexFactory.getIndexModifier().docCount();
    }catch (Exception ex) {
      throw new ApplicationException(ErrorConstant.INDEX_CANNOT_READ_INDEX, ex, ex.toString());
    }
  }
  
  /**
   * Insert a dcoument to lucene.
   * @param doc
   * @throws ApplicationException
   */
  public void addDocument(Document doc) throws ApplicationException {
    if (doc==null) {
      return;
    }
    try {
      IndexFactory.getIndexModifier().addDocument(doc);
    }catch (Exception ex) {
      log.fatal("Error occurs at adding document!", ex);
      throw new ApplicationException(ErrorConstant.INDEX_CANNOT_WRITE_INDEX, ex, ex.toString());
    }
  }
  
  /**
   * Delete lucene by gived term object.
   * @param term
   * @return
   * @throws ApplicationException
   */
  public int deleteDocuments(Term term) throws ApplicationException {
    try {
      return IndexFactory.getIndexModifier().deleteDocuments(term);
    }catch (Exception ex) {
      throw new ApplicationException(ErrorConstant.INDEX_CANNOT_WRITE_INDEX, ex, ex.toString());
    }
  }
  
  /**
   * Insert a document to lucene by dms_document_id, program will load data from database to perform index.
   * @param dmsDocumentID
   * @throws ApplicationException
   */
  public void addDmsDocument(Integer dmsDocumentID) throws ApplicationException {
    Document document = this.getIndexOperation().getDocument(dmsDocumentID);
    if (document!=null) {
      this.addDocument(document);
    }
  }
  
  /**
   * Insert a document to lucene by dms_document objcet, program will load data from database to perform index.
   * @param dmsDocument
   * @throws ApplicationException
   */
  public void addDmsDocument(DmsDocument dmsDocument) throws ApplicationException {
    Document document = this.getIndexOperation().getDocument(dmsDocument);
    if (document!=null) {
      this.addDocument(document);
    }
  }
  
  /**
   * Insert a document to lucene by gived data term.
   * @param dmsDocument
   * @param dmsContent
   * @param data
   * @param documentDetailList
   * @throws ApplicationException
   */
  public void addDmsDocument(DmsDocument dmsDocument, DmsContent dmsContent, InputStream data, List documentDetailList) throws ApplicationException {
    Document document = getIndexOperation().getDocument(dmsDocument, dmsContent, data, documentDetailList);
    if (document!=null) {
      this.addDocument(document);
    }
  }
  
  /**
   * Insert a document to lucene by gived datas.
   * If gived property is not null, property value would use directly to perform index. 
   * If gived property is null, property value will reload form database.
   * @param dmsDocument
   * @param dmsContent
   * @param data
   * @param documentDetailList
   * @param basePath
   * @throws ApplicationException
   */
  public void addDmsDocument(DmsDocument dmsDocument, DmsContent dmsContent, InputStream data, List documentDetailList, String basePath) throws ApplicationException {
    Document document = getIndexOperation().getDocument(dmsDocument, dmsContent, data, documentDetailList, basePath);
    if (document!=null) {
      this.addDocument(document);
    }
  }
  
  /**
   * Insert documents by gived dms_document_id list, program will load data from database to perform index.
   * @param dmsDocumentIds
   * @throws ApplicationException
   */
  public void addDmsDocument(List dmsDocumentIds) throws ApplicationException {
    int batchIndex = 0;
    int size = dmsDocumentIds.size();
    while (true) {
      int index_start = (batchIndex++) * batchSize;
      if (index_start>=size) { break; }
      int index_end = (index_start+batchSize)>size ? size : (index_start+batchSize);
      List batchDmsDocumentIds = dmsDocumentIds.subList(index_start, index_end);
      List documents = getIndexOperation().getDocuments(batchDmsDocumentIds);
      if (documents!=null && !documents.isEmpty()) {
        for (Iterator it = documents.iterator(); it.hasNext();) {
          this.addDocument((Document) it.next());
        }
      }
    }
  }
  
  /**
   * Delete document by gived dms_document_id. All uid equal gived, would be deleted.
   * @param dmsDocumentId
   * @return
   * @throws ApplicationException
   */
  public int deleteDmsDocument(Integer dmsDocumentId) throws ApplicationException {
    return this.deleteDocuments(new Term(FieldNames.UID, dmsDocumentId.toString()));
  }
  
  /**
   * Delete document by gived dms_document objcet. All uid equal dmsDocument.getID() would be deleted. 
   * @param dmsDocument
   * @return
   * @throws ApplicationException
   */
  public int deleteDmsDocument(DmsDocument dmsDocument) throws ApplicationException {
    return this.deleteDocuments(new Term(FieldNames.UID, dmsDocument.getID().toString()));
  }
  
  /**
   * Delete all documents which uid contains in gived list.
   * @param dmsDocumentIds
   * @return
   * @throws ApplicationException
   */
  public int deleteDmsDocument(List dmsDocumentIds) throws ApplicationException {
    int num = 0;
    for (Iterator it = dmsDocumentIds.iterator(); it.hasNext();) {
      num += this.deleteDocuments(new Term(FieldNames.UID, it.next().toString()));
    }
    return num;
  }
  

  /**
   * Update doucment content field value. 
   * @param dmsDocument
   * @param data
   * @param extension
   * @throws ApplicationException
   */
  public void updateDmsDocument(DmsDocument dmsDocument, InputStream data, String extension) throws ApplicationException {
    try {
      Term term = new Term(FieldNames.UID, dmsDocument.getID().toString());
      Hits hits = this.getIndexSearcher().search(new TermQuery(term));
      if (hits.length()>0) {
        Document doc = hits.doc(0);
        String[] fieldNames = new String[] {FieldNames.CONTENT, FieldNames.EXTENSION};
        String[] fieldValues = new String[2];
        fieldValues[0] = IndexConverter.getText(data, (extension==null?"TXT":extension));
        fieldValues[1] = extension;
        doc = getIndexOperation().resetDocument(doc, fieldNames, fieldValues);
        this.deleteDocuments(term);
        this.addDocument(doc);
      }
    }catch (ApplicationException ex) {
      throw ex;
    }catch (Exception ex) {
      throw new ApplicationException(ErrorConstant.INDEX_UNEXPECTED_EXCEPTION, ex, ex.toString());
    }
  }

  /**
   * Reset document field by gived field names and values.
   * @param document
   * @param fieldNames
   * @param fieldValues
   * @return
   * @throws Exception
   */
  public Document resetDocument(Document document, String[] fieldNames, String[] fieldValues) throws Exception {
    return getIndexOperation().resetDocument(document, fieldNames, fieldValues);
  }
  
  /**
   * Reset document content field by gived input strem and it's extension. 
   * @param document
   * @param data
   * @param extension
   * @return
   * @throws Exception
   */
  public Document resetDocumentContent(Document document, InputStream data, String extension) throws Exception {
    String content = IndexConverter.getText(data, (extension==null?"TXT":extension));
    return getIndexOperation().resetDocument(document, new String[]{FieldNames.CONTENT}, new String[]{content});
  }
  
  /**
   * Reset document standard field value by gived dmsDocument object.
   * Standard field list at the Field.ALL_STANDARD_NAMES array object.
   * @param document
   * @param dmsDocument
   * @return
   * @throws Exception
   */
  public Document resetDocumentStandardFields(Document document, DmsDocument dmsDocument) throws Exception {
    return getIndexOperation().resetDocumentStandardFields(document, dmsDocument, null);
  }
  
  /**
   * Reset document standard field value by gived dmsDocument object and extention.
   * extension parameter is the document file extension.
   * @param document
   * @param dmsDocument
   * @param extension
   * @return
   * @throws Exception
   */
  public Document resetDocumentStandardFields(Document document, DmsDocument dmsDocument, String extension) throws Exception {
    return getIndexOperation().resetDocumentStandardFields(document, dmsDocument, extension);
  }
  
  /**
   * Reset document default defined index fields.
   * @param document
   * @param dmsDocument
   * @return
   * @throws Exception
   */
  public Document resetDocumentDDFIndexFields(Document document, DmsDocument dmsDocument) throws Exception {
    return getIndexOperation().resetDocumentDDFIndexFields(document, dmsDocument);
  }
  
  /**
   * Reset document user defined index fields.
   * @param document
   * @param documentDetailList
   * @return
   * @throws Exception
   */
  public Document resetDocumentUDFIndexFields(Document document, List documentDetailList) throws Exception {
    return getIndexOperation().resetDocumentUDFIndexFields(document, documentDetailList);
  }
  
  public InputStream getContent(Integer documentID) throws ApplicationException {
    try{      
      InputStream data = null;
      Query query =  new TermQuery(new Term(FieldNames.UID, documentID.toString()));
      Hits hits = this.getIndexSearcher().search(query);
      if (hits.length() > 0) {
        Document doc = hits.doc(0);
        if (doc.get(FieldNames.CONTENT)!=null) {  
          byte[] bAry = (doc.get(FieldNames.CONTENT)).getBytes("UTF-8");
          data = new ByteArrayInputStream(bAry); 
        }else{
          data = null;
        }
      }
      return data;
    }catch (ApplicationException ex) {
      throw ex;
    }catch (Exception ex) {
      throw new ApplicationException(ErrorConstant.INDEX_UNEXPECTED_EXCEPTION, ex, ex.toString());
    }
  }
  
  /**
   * Get content string from lucene index whose uid equal gived dms_document_id.
   * @param documentID
   * @return
   * @throws ApplicationException
   */
  public String getContentString(Integer documentID) throws ApplicationException {
    try{
      Query query =  new TermQuery(new Term(FieldNames.UID, documentID.toString()));
      Hits hits = this.getIndexSearcher().search(query);
      String result = null;
      if (hits.length() > 0) {
        Document doc = hits.doc(0);
        result = doc.get(FieldNames.CONTENT);
      }
      return result;
    }catch (ApplicationException ex) {
      throw ex;
    }catch (Exception ex) {
      throw new ApplicationException(ErrorConstant.INDEX_UNEXPECTED_EXCEPTION, ex, ex.toString());
    }
  }
  
  public Hits query(Term term) throws ApplicationException {
    try {
      return this.getIndexSearcher().search(new TermQuery(term));
    }catch (Exception ex) {
      throw new ApplicationException(ErrorConstant.INDEX_CANNOT_READ_INDEX, ex, ex.toString());
    }
  }
  
  /**
   * Query document object by dms_document_id.
   * @param documentId
   * @return
   * @throws ApplicationException
   */
  public Document queryDocument(Integer documentId) throws ApplicationException {
    try {
      Hits hits = this.getIndexSearcher().search(new TermQuery(new Term(FieldNames.UID, documentId.toString())));
      if (hits.length()>0) {
        return hits.doc(0);
      }else {
        return null;
      }
    }catch (Exception ex) {
      throw new ApplicationException(ErrorConstant.INDEX_CANNOT_READ_INDEX, ex, ex.toString());
    }
  }
  
  /**
   * Query IndexDocument by dms_document id.
   * @param documentId
   * @return
   * @throws ApplicationException
   */
  public IndexDocument queryIndexDocument(Integer documentId) throws ApplicationException {
    try {
      Hits hits = this.getIndexSearcher().search(new TermQuery(new Term(FieldNames.UID, documentId.toString())));
      if (hits.length()>0) {
        return this.getIndexDocument(hits.doc(0), hits.id(0), hits.score(0));
      }else {
        return null;
      }
    }catch (Exception ex) {
      throw new ApplicationException(ErrorConstant.INDEX_CANNOT_READ_INDEX, ex, ex.toString());
    }
  }
  
  /**
   * Query documents list by query term, sort object, and pager information.
   * @param query
   * @param sort
   * @param pager
   * @return
   * @throws ApplicationException
   */
  public List queryIndexDocuments(Query query, Sort sort, Pager pager) throws ApplicationException {
    try {
      Hits hits = this.getIndexSearcher().search(query, sort);
      if (hits.length()>0) {
        pager.setRecordCount(hits.length());
        pager.setPageCount((hits.length()-1) / pager.getPageSize() + 1);
        if (pager.getPageIndex()>pager.getPageCount()) {
          pager.setPageIndex(pager.getPageCount());
        }
      }else {
        pager.setRecordCount(0);
        pager.setPageCount(0);
      }
      if (pager.getRecordStartOffset()<0) {
        pager.setRecordStartOffset((pager.getPageIndex()-1) * pager.getPageSize());
      }
      int beg = pager.getRecordStartOffset();
      int end = (beg+pager.getPageSize())>pager.getRecordCount() ? pager.getRecordCount() : (beg + pager.getPageSize()); 
      List result = new ArrayList();
      for (int i=beg; i<end; i++) {
        result.add(this.getIndexDocument(hits.doc(i), hits.id(i), hits.score(i)));
      }
      return result;
    }catch (Exception ex) {
      throw new ApplicationException(ErrorConstant.INDEX_CANNOT_READ_INDEX, ex, ex.toString());
    }
  }
  
  /**
   * Highlight searching result.
   * @param query
   * @param lstKeywords
   * @param query
   * @param text
   * @param fieldName
   * @return
   * @throws Exception
   */
  public String getHtmlHighLight(Query query, List lstKeywords, int hitId, String text, String fieldName) throws Exception {
    try {
      if (Utility.isEmpty(text)) {
        return null;
      }
      DmsHighlighter highlighter = new DmsHighlighter(this.getReadOnlyIndexReader(), lstKeywords);
      return highlighter.highlight(query, hitId, text, fieldName);
    }catch (Exception ex) {
      log.error(ex, ex);
      return null;
    }
  }
  
  /**
   * Close manager, release IndexSearcher, IndexModifier, and set null.
   */
  public void close() {
    this.close(false);
  }
  
  /**
   * Close manager, release IndexSearcher, IndexModifier, and set null.
   */
  public void close(boolean optimize) {
    // close indexmodifier/release lock
    try { IndexFactory.close(optimize); }catch (Exception ignore){}
    
    // close indexReader
    try { if (indexReadOnlyReader!=null) {
      indexReadOnlyReader.close();
    } } catch (Exception ignore) {} finally {indexReadOnlyReader=null;}
    
    // close searcher 
    try { if (indexSearcher!=null) {
      indexSearcher.close();
    } }catch (Exception ignore) {} finally {indexSearcher=null;}
  }
  
  private IndexDocument getIndexDocument(Document doc, int id, float score) throws Exception {
    IndexDocument indexDocument = new IndexDocument();
    indexDocument.setId(id);
    indexDocument.setScore(score);
    indexDocument.setUid(doc.get(FieldNames.UID));
    indexDocument.setDocumentName(doc.get(FieldNames.DOCUMENT_NAME));
    indexDocument.setDocumentType(doc.get(FieldNames.DOCUMENT_TYPE));//Add a field. EIP-1296 lee 07/01/18
    indexDocument.setEffective_start_date(doc.get(FieldNames.EFFECTIVE_START_DATE));//Add a field. EIP-1129  Jim Zhou   2007/05/22
    indexDocument.setEffective_end_date(doc.get(FieldNames.EFFECTIVE_END_DATE));//Add a field. EIP-1129  Jim Zhou   2007/05/22
    indexDocument.setRootId(doc.get(FieldNames.ROOT_ID));
    indexDocument.setReferenceNo(doc.get(FieldNames.REFERENCE_NO));
    indexDocument.setDescription(doc.get(FieldNames.DESCRIPTION));
    indexDocument.setExtension(doc.get(FieldNames.EXTENSION));
    indexDocument.setItemSize(doc.get(FieldNames.ITEM_SIZE));
    indexDocument.setItemStatus(doc.get(FieldNames.ITEM_STATUS));
    indexDocument.setUpdaterId(doc.get(FieldNames.UPDATER_ID));
    indexDocument.setUpdateDate(doc.get(FieldNames.UPDATE_DATE));
    indexDocument.setContent(doc.get(FieldNames.CONTENT));
    
    int defaultIndexSize = this.getIndexOperation().getDefaultIndexSize();
    for (int i=0; i<defaultIndexSize; i++) {
      if (doc.get(FieldNames.DEFAULT_INDEXES+i)!=null) {
        indexDocument.addDefaultIndex(new Integer(i), doc.get(FieldNames.DEFAULT_INDEXES+i));
      }
    }
    
    int userDefinedIndexSize = this.getIndexOperation().getUserDefinedIndexSize();
    for (int i=0; i<userDefinedIndexSize; i++) {
      if (doc.get(FieldNames.USER_DEFINED_INDEXES+i)!=null) {
        indexDocument.addUserDefinedIndex(new Integer(i), doc.get(FieldNames.USER_DEFINED_INDEXES+i));
      }
    }
    
    indexDocument.setUserDefinedIndexId(doc.get(FieldNames.USER_DEFINED_INDEX_ID));
    indexDocument.setUserDefinedIndexDetailIds(doc.get(FieldNames.USER_DEFINED_INDEX_DETAIL_IDS));
    return indexDocument;
  }
}
